[WIP]: [Fix] Per-rank SHM + streaming 1× host peak + FX device placement - #64
Open
cennn wants to merge 56 commits into
Open
[WIP]: [Fix] Per-rank SHM + streaming 1× host peak + FX device placement#64cennn wants to merge 56 commits into
cennn wants to merge 56 commits into
Conversation
_patch_cpu_offload_apply created a single shared-memory file from local_rank=0 and had all ranks read it. With expert parallelism (EP>1), each rank holds a different expert shard; reading rank-0 data on every rank destroyed expert weight diversity and produced garbled video output. Fix: when EP_SIZE>1, fall back to per-rank pin_memory instead of cross-rank shared-memory dedup. Also: move model weights to CUDA before Dynamo tracing (_deep_cuda) so Dynamo captures the fused Triton kernel path instead of the decomposed Python fallback, and extend _fix_graph_device_placement to fix ALL FX nodes with CPU example_values (not just get_attr/placeholder).
…tion Two tests using torch.multiprocessing.spawn with gloo backend: 1. test_shared_memory_overwrites_ep_shards: Reproduces the bug — local_rank=0 writes expert weights to a shared file, all other ranks read it, silently overwriting their own expert shards with rank 0's data. 2. test_ep_fix_preserves_per_rank_shards: Verifies the fix — when EP_SIZE > 1, the shared-memory path is skipped and each rank retains its own expert weights.
ENGINE_CONFIG__EP_SIZE may not be set if the host framework (e.g. disagg_compute_runner) only sets EP_SIZE or configures ep_size programmatically. Fall back to EP_SIZE env var before defaulting to 1.
When EP_SIZE > 1, each rank holds a unique expert shard. The previous fix skipped shared memory entirely and used pin_memory, which was extremely slow for large models (~46GB per rank). Now each rank writes its own shared-memory file to /dev/shm and mmap-reads it back, preserving per-rank expert weights while keeping the speed benefit of shared memory + pin_memory_in_place on already-resident pages. For EP_SIZE <= 1, the original rank-0-writes-all-read scheme is retained (all ranks have identical weights). Also updates the regression test to verify the per-rank shm path.
…_apply With EP>1, all 8 ranks simultaneously created ~43GB flat_buffer + wrote ~43GB to /dev/shm = ~87GB per rank x 8 = ~700GB, exceeding 512Gi container limit. Fix: serialize writes across ranks (one at a time) and write directly into mmap file (no flat_buffer). Peak memory drops from ~700GB to ~392GB.
1. _force_cpu: skip GPU roundtrip for CPU tensors when fn only changes device (not dtype). Reduces peak host memory during model.cuda() by avoiding temporary CUDA host allocations for every parameter. 2. MAGI_OFFLOAD_SKIP_SHM: when set to "1", skip shared memory creation and pin_memory_in_place entirely. Params remain as regular CPU tensors. This allows OffloadExecutor to work on memory-constrained nodes (e.g. 5090 with 512Gi container limit for 8x EP ranks) where the shm+pin overhead causes OOM.
When converting CPU example_value metadata to CUDA for Inductor, the .to(device) call strips torch.nn.Parameter wrapping. This caused OffloadExecutor to misidentify all model weights as regular input tensors, loading all 43.6GB onto GPU simultaneously instead of offloading per-submodule — OOM on 5090 (31GB VRAM). Re-wrap the converted FakeTensor in nn.Parameter to preserve type info.
- OffloadExecutor: log per-step H2D vs compute breakdown when MAGI_OFFLOAD_DEBUG=1 (cuda.synchronize between prefetch and compute for accurate wall-clock split) - _patch_cpu_offload_apply: support MAGI_OFFLOAD_PIN_BUDGET_GB env var Pin up to N GB of weights per rank via cudaHostRegister (no SHM copy) for faster async H2D while staying within host memory budget
Log start/end time for each rank during staggered pin, plus OS memlock limit. Helps debug slow NFS page faults during cudaHostRegister.
Replace sequential 1-rank-at-a-time pinning with parallel waves. Auto-detects max concurrent ranks: total_ram/2 / per_rank_param_size. Override via MAGI_OFFLOAD_PIN_CONCURRENCY env var. 512GB node, 43.36GB/rank → concurrency=5, 2 waves instead of 8. Expected pin time: ~6min vs ~22min sequential.
The per-submodule cuda.synchronize() barriers prevented H2D/compute pipeline overlap, reducing production throughput. Profiling data has been collected; this debug scaffolding is no longer needed.
- Revert offload_warpper.py (all changes were unused imports after debug removal) - Remove dead offload() __dict__ branch (call sites only pass tuple/dict) - Extract 80-line inline pin logic into _staggered_pin_memory() helper - magi_backend.py: use module-level os/magi_logger instead of inline imports - Use %-formatting instead of f-string for logger calls
…fload_apply - _shm_path(): centralize /dev/shm path construction - _pack_params_flat(): copy named tensors into contiguous buffer - _split_flat_to_params(): split flat buffer back to named param views - _create_shm_tensor(): create mmap file + pack in one call - Unify EP>1 and EP<=1 serialization (both use mmap now, remove numpy bf16/fp8 workaround) - _patch_cpu_offload_apply SHM logic: ~135 lines -> ~45 lines
- Merge per_rank_shm and shared branches into single helper - Collapse 3-way dispatch to 2-way (skip_shm vs materialize) - Single cleanup point for del/gc.collect() - Eliminate duplicated pin/append/split/remove/load_state_dict
1 task
…tracing After _deep_cuda removal (e0c7277), Dynamo traces with CPU tensors and specialises .to(x.device) as .to(device('cpu')) — a hardcoded literal in the FX graph. _fix_graph_device_placement already moves example_values to CUDA, but these baked .to(cpu) nodes remained, causing index_select(CUDA, CPU) → BackendCompilerFailed during PiecewiseCompileInterpreter.run(). Extend _fix_graph_device_placement to also rewrite: - call_method('to', device('cpu')) → call_method('to', device('cuda')) - call_function(..., device='cpu') → call_function(..., device='cuda') Add regression test (test_fix_to_cpu_in_graph.py) that: 1. Confirms metadata-only fix still produces the device mismatch 2. Verifies the full rewrite resolves the error 3. Ensures .to(dtype) calls are not affected
…ARE_WEIGHTS - Add _compute_weights_fingerprint(): SHA256 over param names + shapes + head/tail sampled data (~1 KB per param, < 1s for any model size) - Add _all_ranks_same_weights(): all_gather fingerprints across ranks - Auto-detect per_rank in _patch_cpu_offload_apply: if all ranks have identical weights -> shared mmap; otherwise per-rank mmap - Rename env override: MAGI_COMPILE_OFFLOAD_CONFIG__FORCE_PER_RANK_WEIGHTS (replaces SHM_SHARE_WEIGHTS; None=auto, True=force per-rank, False=force share) - Update OffloadConfig: shm_share_weights -> force_per_rank_weights (Optional[bool]) - Add 4 fingerprint unit tests (identical, different, deterministic, single-element)
…mpat) Extract get_cpu_gloo_group() into utils/dist_utils.py, shared by _all_ranks_same_weights and _get_cost_sync_group. Fixes RuntimeError on NCCL-default process groups where CPU tensors are rejected.
… to per_rank=True)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
model_cpu_offloadmaterializes weights into/dev/shmso ranks on one node can share pinned pages. Onmain, rank 0 writes a single flat file per dtype and every rank maps it — correct when all ranks hold the same weights, but when ranks hold different shards (expert parallelism), every shard is silently replaced with rank 0's data. MoE routing still runs, experts are numerically wrong, producing garbled video.The same
mainpath allocates a fullflat_buffer = torch.zeros(total_numel)before writing to disk, so host anonymous memory peaks at ~2× model size. For a 200 GB+ MoE model across 8 ranks this means ~700 GB peak — dangerously close to host OOM.Separately,
mainonly rewritesdevice=kwargs on factory functions (torch.empty,torch.zeros, …) in the FX graph. Offload tracing also emits.to(device("cpu"))call_method nodes and leaves CPUexample_valuemetadata on arbitrary nodes, which Inductor cannot handle — it raisesFakeTensor device mismatchon ops likeaten.index_selectreceiving both cuda and cpu arguments.Changes
1. Auto-detect per-rank SHM via weight fingerprint
Each rank computes a SHA256 fingerprint (param names + shapes + sampled head/tail data, < 1 s for any model size), then
dist.all_gathercompares across ranks. All identical → single shared mmap; any differ → per-rank mmap. Zero configuration needed for both DP/CP and EP scenarios.Override:
MAGI_COMPILE_OFFLOAD_CONFIG__FORCE_PER_RANK_WEIGHTSenv var (1/0) bypasses auto-detection when needed.2. Streaming copy-and-replace (1× host peak)
_stream_copy_and_replacecopies each parameter into the mmap and immediately replaces it in the module, so only one parameter's worth of duplication exists at any moment. The oldflat_buffer+tofile()+load_state_dictpath is eliminated.main(flat_buffer)(measured via
smaps_rollup Anonymouson 64 MB model in isolated subprocess)3. Full FX device placement rewrite
fix_graph_device_placement(module-level pure function) rewrites:call_functionnodes withdevice=cpukwargs (covers all ops, not just factory functions)call_method.to(cpu)nodes (positional args and kwargs)example_valuemetadata, recursively through nested list/tupleOther
offload()recursively penetrates plain objects'__dict__to move CUDA tensors to CPU, skippingnn.Moduleto avoid interfering with model internals._force_cpuskips the GPU round-trip when the tensor is already on CPU andfndoes not change dtype.host_memory.py: lightweight/proc-based host memory introspection (fmt_host_mem()), logged at key offload stages.OffloadConfig.force_per_rank_weights:None(auto-detect) /True/False, exposed as Pydantic field with env var support viaBaseSettings.Evidence
samples_5req_diverse.json.Tests (+701 lines, 3 new files)
test_ep_shared_memory.py: 2-rank gloo repro —per_rank=Falsecorrupts shards,per_rank=Truepreserves; 4 fingerprint auto-detection tests.test_fix_to_cpu_in_graph.py: FX graph.to(cpu)+device=cpurewrite — metadata-only fix fails, full rewrite succeeds.test_shm_memory_peak.py: subprocess-isolatedsmaps_rollup Anonymousmeasurement — batch shows ~1× growth, streaming shows ~0× growth.